/*
This source file is part of the open source TripleGasm project
Copyright (C) 2010 Roma Kovalev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

For the latest info, see http://code.google.com/p/triplegasm
You can contact the author(s) via e-mail: endiannesz@gmail.com
*/

#include <cstdlib>
#include <ctime>
#include "triplegasm.hpp"

using namespace OgreBites;
using namespace Ogre;
using namespace OIS;
using namespace std;
using namespace TripleGasm;

// Todo:
// Convert all 3d spheres into 2d planes with textures if 2d mode is selected
// Add a finite state machine class that will handle all the states
// Convert local matrix, so it stores SceneNodes* instead of BaseSpheres*
// This will help in a few rare situations when there are duplicate SceneNodes
// in one location

Project::Project() : 
mTimeTillNextSphere(0.0),
mpDebugSphere(nullptr), 
mIsPaused(true),
mIsHelpMenu(false)
{
  srand(time(0));
}

Project::~Project()
{
  mTrayMgr->destroyAllWidgets();
}

bool Project::frameRenderingQueued(const FrameEvent& evt)
{
  bool ret= Core::frameRenderingQueued(evt);

  if (!mIsPaused)
  {
    // Every # seconds a new sphere object is added to the scene.
    if ((mTimeTillNextSphere -= evt.timeSinceLastFrame) <= 0)
    {
      mSpheres.push_back(new Ball(mSceneMgr));
      mTimeTillNextSphere= 5;
    }

    // If a sphere is added to the manipulator, its pointer is removed.
    std::vector<std::vector<Ball*>::const_iterator> eraseThese;

    for (auto it= mSpheres.begin(); it != mSpheres.end(); ++it)
    {
      // Move each sphere
      (*it)->Move(evt);

      // And check if its possible to add it to the manipulator
      if (mManipulator.AddSphere( (*it) /*Pointer to a sphere object*/ ))
        // The sphere pointer was passed to the manipulator,
        // so its safe to remove it
        eraseThese.push_back(it);
    }

    if (eraseThese.size())
    {
      for_each(eraseThese.begin(), eraseThese.end(),
        [&](std::vector<Ball*>::const_iterator i){mSpheres.erase(i);});
    }

    // Rotate the manipulator
    mManipulator.Move(evt);
  }

#ifdef _DEBUG
  DialogData(); // Shows a debug overlay
#endif

  return ret;
}

bool Project::keyPressed(const OIS::KeyEvent &arg)
{
  auto ret= Core::keyPressed(arg);

  if (mIsHelpMenu)
  {
    mTrayMgr->destroyWidget("LabelHelp");
    mTrayMgr->clearTray(TL_CENTER);
    mIsHelpMenu= false;
    ShowMenu();
    return true;
  }

#ifdef _DEBUG
  if (arg.key == KC_F && mpDetailsPanel->isVisible())
  {
    mTrayMgr->moveWidgetToTray(mpDetailsPanel, TL_NONE);
    mpDetailsPanel->hide();
  }
  else if (arg.key == KC_F)
  {
    mTrayMgr->moveWidgetToTray(mpDetailsPanel, TL_TOPLEFT);
    mpDetailsPanel->show();
  }
#endif

  switch (arg.key)
  {
  case OIS::KC_LEFT: case OIS::KC_A:
    mManipulator.SetSpeed(Manipulator::MANIPULATOR_SPEED);
    break;
  case OIS::KC_RIGHT: case OIS::KC_D:
    mManipulator.SetSpeed(-Manipulator::MANIPULATOR_SPEED);
    break;
  case OIS::KC_P:
    mIsPaused= !mIsPaused;
  }

  if (arg.key == KC_ESCAPE && !mIsPaused)
  {
    mIsPaused= true;
    mTrayMgr->moveWidgetToTray(mpButtonContinue, TL_CENTER);
    mpButtonContinue->show();
    ShowMenu();
    ret= true; // Return true, don't exit yet.
  }
  else if (arg.key == KC_ESCAPE && mIsPaused)
  {
    mIsPaused= false;
    HideMenu();
    ret= true; // Return true, don't exit yet.
  }

  return ret;
}

bool Project::keyReleased(const OIS::KeyEvent &arg)
{
  auto ret= Core::keyReleased(arg);

  switch (arg.key)
  {
  case OIS::KC_LEFT: case OIS::KC_RIGHT: case OIS::KC_A: case OIS::KC_D:
    mManipulator.SetSpeed(0);
  }

  return ret;
}

void Project::Create()
{
  BaseSphere::CreateSphere();

  mCamera->setPosition(0.0, 0.0, 100.0);
  // Look back along -Z
  mCamera->lookAt(0.0, 0.0, -100.0);

  // Create a light
  Light* l= mSceneMgr->createLight("OmniLight");
  l->setPosition(0, 0, 100);
  l->setType(Light::LT_POINT);
  l->setDiffuseColour(ColourValue::White);
  l->setSpecularColour(ColourValue::White);

  // Don't put this in the constructor, the "this" pointer is invalid
  mManipulator= Manipulator(mSceneMgr);
  mManipulator.SetLocalMatrixElement(&mManipulator);
  mManipulator.SetNodeData(static_cast<BaseSphere*>(&mManipulator));

  // Create a plan for the background, can't use SkyPlain because it
  // ruins the aspect ratio.
  Ogre::Plane plane(Ogre::Vector3::UNIT_Z, -1100);
  Ogre::MeshManager::getSingleton().createPlane("Dome",
    Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
    plane, 1680, 1050, 1, 1, true, 1, 1, 1, Ogre::Vector3::UNIT_Y);

  auto p= mSceneMgr->createEntity("DomePlain", "Dome");
  p->setMaterialName("dome");
  mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(p);

  mpRay= mSceneMgr->createRayQuery(Ray());
  mpRay->setSortByDistance(true);
  mpRay->setQueryMask(BaseSphere::query_flags::SPHERE);

  mpButton2D     = mTrayMgr->createButton(TL_CENTER, "Button2D"     , "Start 2D mode", 200);
  mpButton3D     = mTrayMgr->createButton(TL_CENTER, "Button3D"     , "Start 3D mode", 200);
  mpButtonOptions= mTrayMgr->createButton(TL_CENTER, "ButtonOptions", "Options"      , 200);
  mpButtonScores = mTrayMgr->createButton(TL_CENTER, "ButtonScore"  , "Scores"       , 200);
  mpButtonExit   = mTrayMgr->createButton(TL_CENTER, "ButtonExit"   , "Exit"         , 200);
  mpButtonHelp   = mTrayMgr->createButton(TL_CENTER, "ButtonHelp"   , "Help"         , 200);
  mpButtonContinue= mTrayMgr->createButton(TL_NONE, "ButtonCont", "Continue", 200);
  mpButtonContinue->hide();
  mpButtonOk= mTrayMgr->createButton(TL_NONE, "ButtonOk", "Ok", 100);
  mpButtonOk->hide();

#ifdef _DEBUG
  // Create a params panel for displaying sample details
  StringVector items;
  items.push_back("Sphere type");
  items.push_back("Left point");
  items.push_back("Right point");
  items.push_back("Bottom point");
  items.push_back("Top point");
  items.push_back("Matrix pos");
  items.push_back("X");
  items.push_back("Y");
  items.push_back("Z");
  items.push_back("Node");
  items.push_back("Is checked");

  mpDetailsPanel= mTrayMgr->createParamsPanel(TL_TOPLEFT, "DetailsPanel", 250, items);
#endif

  mpRendererMenu = mTrayMgr->createLongSelectMenu(
    TL_NONE, 
    "RendererMenu", 
    "Render System",  450,  240,  10);
  mpRendererMenu->hide();

  Ogre::StringVector rsNames;
  Ogre::RenderSystemList rsList = mRoot->getAvailableRenderers();
  for (unsigned int i = 0; i < rsList.size(); i++)
    rsNames.push_back(rsList[i]->getName());

  for (BYTE i= 0; i <= 10; ++i)
    mpDetailsPanel->setParamValue(i, "No info");

  mpRendererMenu->setItems(rsNames);
  windowResized(mWindow);
}

#ifdef _DEBUG
void Project::DialogData()
{
  // If details panel is visible, then update its contents.
  if (mpDetailsPanel->isVisible())
  {
    if (mpDebugSphere != nullptr)
    {
      mpDetailsPanel->setParamValue(0, StringConverter::toString(
        mpDebugSphere->GetType()));

      for (BYTE i= 0; i < 4; ++i)
      {
        SceneNode* p= mpDebugSphere->mpAttachPoints[i];
        mpDetailsPanel->setParamValue(1 + i,  p ? p->getName() : " " );
      }

      mpDetailsPanel->setParamValue(5, StringConverter::toString(
        mpDebugSphere->GetGraphPos().first) + " : " + 
        StringConverter::toString(mpDebugSphere->GetGraphPos().second));

      mpDetailsPanel->setParamValue(6, StringConverter::toString(
        mpDebugSphere->GetDerivedPosition().x));
      mpDetailsPanel->setParamValue(7, StringConverter::toString(
        mpDebugSphere->GetDerivedPosition().y));
      mpDetailsPanel->setParamValue(8, StringConverter::toString(
        mpDebugSphere->GetDerivedPosition().z));

      mpDetailsPanel->setParamValue(9, mpDebugSphere->GetName());

      mpDetailsPanel->setParamValue(10, StringConverter::toString(
        mpDebugSphere->isChecked));
    }
  }
}
#endif

bool TripleGasm::Project::mouseMoved(const OIS::MouseEvent &arg)
{
  bool ret= Core::mouseMoved(arg);
#ifdef _DEBUG
  // This is only required for the debug overlay
  // It gets the the pointer to the sphere under your mouse cursor.
  if (mpDetailsPanel->isVisible())
  {
    Ray mouseRay= mCamera->getCameraToViewportRay(
      arg.state.X.abs/float(arg.state.width), 
      arg.state.Y.abs/float(arg.state.height));
    mpRay->setRay(mouseRay);

    // Execute query
    RaySceneQueryResult &result= mpRay->execute();
    RaySceneQueryResult::iterator itr= result.begin();
    if (itr != result.end() && itr->movable)
    {
      SceneNode* tempNode= itr->movable->getParentSceneNode();
      auto tempData= tempNode->getUserObjectBindings().getUserAny();
      if (!tempData.isEmpty())
        mpDebugSphere= any_cast<BaseSphere*>(tempData);
    }
    else
      mpDebugSphere= nullptr;
  }
#endif
  return ret;
}

void Project::HideMenu()
{
  mTrayMgr->moveWidgetToTray(mpButton2D     , TL_NONE);
  mTrayMgr->moveWidgetToTray(mpButton3D     , TL_NONE);
  mTrayMgr->moveWidgetToTray(mpButtonExit   , TL_NONE);
  mTrayMgr->moveWidgetToTray(mpButtonOptions, TL_NONE);
  mTrayMgr->moveWidgetToTray(mpButtonScores , TL_NONE);
  mTrayMgr->moveWidgetToTray(mpButtonHelp   , TL_NONE);
  mpButton2D     ->hide();
  mpButton3D     ->hide();
  mpButtonExit   ->hide();
  mpButtonOptions->hide();
  mpButtonScores ->hide();
  mpButtonHelp   ->hide();
  mTrayMgr->clearTray(TL_CENTER);
}

void Project::ShowMenu()
{
  mTrayMgr->moveWidgetToTray(mpButton2D     , TL_CENTER);
  mTrayMgr->moveWidgetToTray(mpButton3D     , TL_CENTER);
  mTrayMgr->moveWidgetToTray(mpButtonOptions, TL_CENTER);
  mTrayMgr->moveWidgetToTray(mpButtonScores , TL_CENTER);
  mTrayMgr->moveWidgetToTray(mpButtonExit   , TL_CENTER);
  mTrayMgr->moveWidgetToTray(mpButtonHelp   , TL_CENTER);
  mpButton2D     ->show();
  mpButton3D     ->show();
  mpButtonExit   ->show();
  mpButtonOptions->show();
  mpButtonScores ->show();
  mpButtonHelp   ->show();
}

// worldPos = inverseViewMatrix * rayVector + invCamOrientation*camPosition

void Project::buttonHit(OgreBites::Button* pButton)
{
  if (pButton == mpButtonExit)
    mShutDown= true;
  else if (pButton == mpButton2D)
  {
    mIsPaused= false;
    HideMenu();
  }
  else if (pButton == mpButtonOptions)
  {
    HideMenu();
    mTrayMgr->moveWidgetToTray(mpRendererMenu, TL_CENTER);
    mpRendererMenu->show();

    Ogre::ConfigOptionMap& options= mRoot->getRenderSystemByName(
      mpRendererMenu->getSelectedItem())->getConfigOptions();

    unsigned int i = 0;

    // create all the config option select menus
    for (auto it= options.begin(); it != options.end(); ++it)
    {
      ++i;
      SelectMenu* optionMenu= mTrayMgr->createLongSelectMenu
        (TL_CENTER, "ConfigOption" + Ogre::StringConverter::toString(i),
        it->first, 450, 240, 10);
      optionMenu->setItems(it->second.possibleValues);

      // If the current config value is not in the menu, add it
      try
      {
        optionMenu->selectItem(it->second.currentValue);
      }
      catch (Ogre::Exception& e)
      {
        optionMenu->addItem(it->second.currentValue);
        optionMenu->selectItem(it->second.currentValue);
      }
    }

    mpRendererMenu->selectItem(mRoot->getRenderSystem()->getName());

    mTrayMgr->moveWidgetToTray(mpButtonOk, TL_CENTER);
    mpButtonOk->show();

    windowResized(mWindow);
  }
  else if (pButton == mpButtonOk)   // Apply any changes made in the configuration screen
  {
    bool reset= false;

    Ogre::ConfigOptionMap& options=
      mRoot->getRenderSystemByName(mpRendererMenu->getSelectedItem())->getConfigOptions();

    Ogre::NameValuePairList newOptions;

    // Render system changed -> Reset
    if (mpRendererMenu->getSelectedItem() != mRoot->getRenderSystem()->getName()) 
      reset= true;

    // Something else changed -> Reset
    for (BYTE i= 3; // -1 because the last item is the O.K. button
      i < mTrayMgr->getNumWidgets(mpRendererMenu->getTrayLocation()) - 1;
      ++i)
    {
      SelectMenu* menu = (SelectMenu*)mTrayMgr->getWidget(mpRendererMenu->getTrayLocation(), i);
      if (menu->getSelectedItem() != options[menu->getCaption()].currentValue) 
        reset= true;
      newOptions[menu->getCaption()] = menu->getSelectedItem();
    }

    mTrayMgr->removeWidgetFromTray(mpButtonOk);
    mpButtonOk->hide();

    // Reset with new settings if necessary
    //SampleContext::reconfigure(mpRendererMenu->getSelectedItem(), options);

    // Destroy all but the first widget
    while (mTrayMgr->getNumWidgets(mpRendererMenu->getTrayLocation()) > 1)
      mTrayMgr->destroyWidget(mpRendererMenu->getTrayLocation(), 1);

    mTrayMgr->removeWidgetFromTray(mpRendererMenu);
    mpRendererMenu->hide();

    ShowMenu();

    windowResized(mWindow);
  }
  else if (pButton == mpButtonScores)
  {

  }
  else if (pButton == mpButtonHelp)
  {
    mIsHelpMenu= true;
    HideMenu();
    mTrayMgr->createLabel(TL_CENTER, "LabelHelp", 
      "Every several seconds a new sphere will\n appear on screen and move towards "
      "the center.\n"
      "The goal of the game is to connect 3\n or more spheres of the same colour.\n"
#ifdef _DEBUG 
      "Press F to show the current FPS and debug overlay (debug build only).\n" 
#endif
      "Use your arrow keys or WASD to rotate the manipulator.\n"
      "Press esc to show the menu.\n"
      "Press p to pause the game \n(opening the menu also pauses the game)", 400);
    mTrayMgr->adjustTrays();
  }
  else if (pButton == mpButtonContinue)
  {
    mTrayMgr->moveWidgetToTray(mpButtonContinue, TL_NONE);
    mpButtonContinue->hide();
    HideMenu();
    mIsPaused= false;
  }
}